《Linux – Linux高级编程 – 第二部分 进程与线程》第2章 线程(三)

3 线程高级属性

3.1 线程一次性初始化

有些事需要且只能执行一次(比如互斥量初始化)。通常当初始化应用程序时,可以比较容易地将其放在main函数中。

但当你写一个库函数时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始(pthread_once_t)会比较容易些。

首先要定义一个pthread_once_t变量,这个变量要用宏PTHREAD_ONCE_INIT初始化。然后创建一个与控制变量相关的初始化函数:

pthread_once_t once_control = PTHREAD_ONCE_INIT;

void init_routine ()
{
    //初始化互斥量
    //初始化读写锁
    ......
}

接下来就可以在任何时刻调用pthread_once函数

int pthread_once(pthread_once_t* once_control, void (*init_routine)(void));

功能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。

Linux Threads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次。实际"一次性函数"的执行状态有三种:

NEVER(0)、IN_PROGRESS(1)、DONE (2),用once_control来表示pthread_once()的执行状态:

1)如果once_control初值为0,那么 pthread_once从未执行过,init_routine()函数会执行。

2)如果once_control初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号,因此所有pthread_once ()都会陷入永久 的等待中,init_routine()就无法执行

3)如果once_control设为2,则表示pthread_once()函数已执行过一次,从而所有pthread_once()都会立即返回,init_routine()就没有机会执行,当pthread_once函数成功返回,once_control就会被设置为2。

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>

pthread_once_t once = PTHREAD_ONCE_INIT;

pthread_t tid;

void thread_init()
{
    printf("I am thread_init \n"); 
}

void *thread_fun1(void *arg)
{
    tid = pthread_self();
    printf("I am thread_fun1 0x%x\n",(unsigned int)tid);

    pthread_once(&once,thread_init);
}

void *thread_fun2(void *arg)
{
    sleep(2);
    tid = pthread_self();
    printf("I am thread_fun2 0x%x\n",(unsigned int)tid);
    pthread_once(&once,thread_init);
}

int main(int argc,const char *argv[])
{
    pthread_t p1,p2;
    pthread_create(&p1,NULL,thread_fun1,NULL);
    pthread_create(&p2,NULL,thread_fun2,NULL);

    //阻塞回收子线程
    pthread_join(p1,NULL);
    pthread_join(p2,NULL);

    return 0;
}

结果如下所示:

cQEba8.png

可以看到,thread_init只执行一次。

3.2线程属性

线程属性对象由pthread_attr_init()接口初始化,并由pthread_attr_destory()来销毁,它们的完整定义是:

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destory(pthread_attr_t *attr);

线程属性为一个结构体。详细例如以下:

typedef struct
{   
    int    detachstate;    //线程的分离状态
    int    schedpolicy;  //线程调度策略
    structsched_param schedparam; //线程的调度參数
    int     inheritsched;   //线程的继承性
    int     scope;     //线程的作用域
    size_t    guardsize;   //线程栈末尾的警戒缓冲区大小
    void*    stackaddr;   //线程栈的位置
    size_t    stacksize;    //线程栈的大小
}pthread_attr_t;

一、线程分离状态:detachstate

该属性决定了线程运行任务后以什么方式来结束自己。

方式如下:

(1) PTHREAD_CREATE_DETACHED —— 分离线程

置为分离线程的线程。当不须要被不论什么线程等待,线程运行完任务后,自己自己主动结束线程,并释放资源。

(2) PTHREAD_CREATE_JOINABLE(缺省) —— 可汇合线程

可汇合线程为线程的默认状态,这样的情况下,原有的线程等待创建的线程结束。仅仅有当pthread_join()函数返回时。创建的线程才算终止。才干释放自己占用的系统资源。

二、线程的调度策略:schedpolicy

(1) SCHED_FIFO(先进先出策略)

FIFO线程持续执行,直至有更高优先级的线程就绪,或者线程本身进入堵塞状态。

当FIFO线程堵塞时,系统将其移出就绪队列,恢复后再增加到同优先级就绪队列的末尾。当FIFO线程被高优先级线程抢占时。它在就绪队列中的位置不变。因此一旦高优先级线程终止或堵塞,被抢占的FIFO线程会马上继续执行

(2) SCHED_RR(轮转策略)

每一个RR线程会获得一个时间片,一旦RR线程的时间片耗尽,系统即将移到就绪队列的末尾。

(3) SCHED_OTHER(缺省)

静态优先级为0。不论什么就绪的FIFO线程或RR线程,都会抢占此类线程。

三、调度參数 : sched_param schedparam

这两个函数具有两个參数。第1个參数是指向属性对象的指针。第2个參数是sched_param结构或指向该结构的指针。结构sched_param在文件/usr/include /bits/sched.h中定义例如以下:

struct sched_param
{
    intsched_priority;

};

结构sched_param的子成员sched_priority控制一个优先权值,大的优先权值相应高的优先权。

系统支持的最大和最小优先权值能够用sched_get_priority_max函数和sched_get_priority_min函数分别得到。

注意:假设不是编写实时程序,不建议改动线程的优先级。由于,调度策略是一件很复杂的事情,假设不对使用会导致程序错误,从而导致死锁等问题。

如:在多线程应用程序中为线程设置不同的优先级别,有可能由于共享资源而导致优先级倒置。

四、线程的继承性: inheritsched

(1) PTHREAD_INHERIT_SCHED(缺省) —— 调度属性自创建者线程继承

(2) PTHREAD_EXPLICIT_SCHED —— 调度属性由调度參数和调度策略决定

继承性决定调度的參数是从创建的进程中继承还是使用在schedpolicy和schedparam属性中显式设置的调度信息。Pthreads不为inheritsched指定默认值,因此假设你关心线程的调度策略和參数,必须先设置该属性。

五、线程的作用域:scope

线程的竞争范围。

PTHREAD_SCOPE_SYSTEM ——在系统范围内竞争资源。

PTHREAD_SCOPE_PROCESS(Linux不支持)——在进程范围内竞争资源

六、线程栈末尾的警戒缓冲区大小:guardsize

该属指定线程末尾的警戒缓冲区大小,在缺省的情况下为一个内存页(4096字节)

七、线程栈的位置:stackaddr

八、线程栈的大小:stacksize

那么线程拥有哪些属性呢?一般地,Linux下的线程有:绑定属性、分离属性、调度属性、堆栈大小属性和满占警戒区大小属性。下面我们就分别来介绍这些属性。

3.2.1绑定属性

说到这个绑定属性,就不得不提起另外一个概念:轻进程(Light Weight Process,简称LWP)。轻进程和Linux系统的内核线程拥有相同的概念,属于内核的调度实体。一个轻进程可以控制一个或多个线程。默认情况下,对于一个拥有n个线程的程序,启动多少轻进程,由哪些轻进程来控制哪些线程由操作系统来控制,这种状态被称为非绑定的。那么绑定的含义就很好理解了,只要指定了某个线程“绑”在某个轻进程上,就可以称之为绑定的了。被绑定的线程具有较高的相应速度,因为操作系统的调度主体是轻进程,绑定线程可以保证在需要的时候它总有一个轻进程可用。绑定属性就是干这个用的。

设置绑定属性的接口是pthread_attr_setscope(),它的完整定义是:

int pthread_attr_setscope(pthread_attr_t *attr, int scope);

它有两个参数,第一个就是线程属性对象的指针,第二个就是绑定类型,拥有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。代码2演示了这个属性的使用。

3.2.2分离属性

前面说过线程能够被合并和分离,分离属性就是让线程在创建之前就决定它应该是分离的。如果设置了这个属性,就没有必要调用pthread_join()或pthread_detach()来回收线程资源了。

设置分离属性的接口是pthread_attr_setdetachstate(),它的完整定义是:

pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

它的第二个参数有两个取值:PTHREAD_CREATE_DETACHED(分离的)和PTHREAD_CREATE_JOINABLE(可合并的,也是默认属性)。

#include <stdio.h>
#include <errno.h>
#include <pthread.h>

void *thread_fun1(void *arg)
{
    printf("I am thread_fun1\n");
    return (void *)1;

}

void *thread_fun2(void *arg)
{
​    printf("I am thread_fun2\n");
​    return (void *)2;
}

int main(int argc,const char *argv[])
{
    pthread_t p1,p2;

    //定义属性变量
    pthread_attr_t attr;

    //线程属性初始化
    pthread_attr_init(&attr);

    //设置为分离状态
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    if(pthread_create(&p1,&attr,thread_fun1,NULL) != 0)
    {
        printf("thread_fun1 pthread_create eror\n");
    }
    if(pthread_create(&p2,NULL,thread_fun2,NULL) != 0)
    {
        printf("thread_fun2 pthread_create eror\n");
    }
    //阻塞回收子线程
    if(pthread_join(p1,NULL) !=0 )
    {
        printf("thread_fun1 pthread_join eror\n");
    }

    if(pthread_join(p2,NULL) !=0 )
    {
        printf("thread_fun2 pthread_join eror\n");
    }

    pthread_attr_destroy(&attr);

    return 0;
}

结果如下所示:

cQVxYD.png

可以看到thraed_fun1分离失败。

3.2.3调度属性

线程的调度属性有三个,分别是:算法、优先级和继承权

Linux提供的线程调度算法有三个:轮询、先进先出和其它。其中轮询和先进先出调度算法是POSIX标准所规定,而其他则代表采用Linux自己认为更合适的调度算法,所以默认的调度算法也就是其它了。轮询和先进先出调度算法都属于实时调度算法。轮询指的是时间片轮转,当线程的时间片用完,系统将重新分配时间片,并将它放置在就绪队列尾部,这样可以保证具有相同优先级的轮询任务获得公平的CPU占用时间;先进先出就是先到先服务,一旦线程占用了CPU则一直运行,直到有更高优先级的线程出现或自己放弃。

设置线程调度算法的接口是pthread_attr_setschedpolicy(),它的完整定义是:

pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);

它的第二个参数有三个取值:SCHED_RR(轮询)、SCHED_FIFO(先进先出)和SCHED_OTHER(其它)。

Linux的线程优先级与进程的优先级不一样,进程优先级我们后面再说。Linux的线程优先级是从1到99的数值,数值越大代表优先级越高。而且要注意的是,只有采用SHCED_RR或SCHED_FIFO调度算法时,优先级才有效。对于采用SCHED_OTHER调度算法的线程,其优先级恒为0。

设置线程优先级的接口是pthread_attr_setschedparam(),它的完整定义是:

struct sched_param {
    int sched_priority;
}
int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param);

sched_param结构体的sched_priority字段就是线程的优先级了。

此外,即便采用SCHED_RR或SCHED_FIFO调度算法,线程优先级也不是随便就能设置的。首先,进程必须是以root账号运行的;其次,还需要放弃线程的继承权。什么是继承权呢?就是当创建新的线程时,新线程要继承父线程(创建者线程)的调度属性。如果不希望新线程继承父线程的调度属性,就要放弃继承权。

设置线程继承权的接口是pthread_attr_setinheritsched(),它的完整定义是:

int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);

它的第二个参数有两个取值:PTHREAD_INHERIT_SCHED(拥有继承权)和PTHREAD_EXPLICIT_SCHED(放弃继承权)。新线程在默认情况下是拥有继承权。
下面使用代码展示不同调度算法和不同优先级下各线程的行为,同时也展示如何修改线程的调度属性。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_COUNT 12

void show_thread_policy( int threadno )
{
    int policy;
    struct sched_param param;
    pthread_getschedparam( pthread_self(), &policy, &param );

    switch( policy ){
    case SCHED_OTHER:
        printf( "SCHED_OTHER %d\n", threadno );
        break;
    case SCHED_RR:
        printf( "SCHDE_RR %d\n", threadno );
        break;
    case SCHED_FIFO:
        printf( "SCHED_FIFO %d\n", threadno );
        break;
    default:
        printf( "UNKNOWN\n");
    }
}
void* thread( void *arg )
{
    int i, j;
    long threadno = (long)arg;

    printf( "thread %d start\n", threadno );
    sleep(1);
    show_thread_policy( threadno );

    for( i = 0; i < 10; ++i ) 
    {
        for( j = 0; j < 100000000; ++j ){}
        printf( "thread %d\n", threadno );
    }
    printf( "thread %d exit\n", threadno );

    return NULL;
}

int main( int argc, char *argv[] )
{
    long i;
    pthread_attr_t attr[THREAD_COUNT];
    pthread_t pth[THREAD_COUNT];
    struct sched_param param;

    for( i = 0; i < THREAD_COUNT; ++i )
        pthread_attr_init( &attr[i] );
        for( i = 0; i < THREAD_COUNT / 2; ++i ) 
        {
            param.sched_priority = 10; 

            //设置线程调度算法
            //三个取值:SCHED_RR(轮询)、SCHED_FIFO(先进先出)和SCHED_OTHER(其它)            
            pthread_attr_setschedpolicy( &attr[i], SCHED_RR );

            //设置线程优先级
            pthread_attr_setschedparam( &attr[i], &param );

            //设置线程继承权
            //两个取值:PTHREAD_INHERIT_SCHED(拥有继承权)和PTHREAD_EXPLICIT_SCHED(放弃继承权)
            pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
        }
        for( i = THREAD_COUNT / 2; i < THREAD_COUNT; ++i ) 
        {
            param.sched_priority = 5;                  
            pthread_attr_setschedpolicy( &attr[i], SCHED_RR );
            pthread_attr_setschedparam( &attr[i], &param );
            pthread_attr_setinheritsched( &attr[i], PTHREAD_EXPLICIT_SCHED );
        }
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_create( &pth[i], &attr[i], thread, (void*)i );              
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_join( pth[i], NULL );                    
        for( i = 0; i < THREAD_COUNT; ++i )                    
            pthread_attr_destroy( &attr[i] );                   
    return 0;                           
}

3.2.4堆栈大小属性

从前面的这些例子中可以了解到,线程的主函数与程序的主函数main()有一个很相似的特性,那就是可以拥有局部变量。虽然同一个进程的线程之间是共享内存空间的,但是它的局部变量确并不共享。原因就是局部变量存储在堆栈中,而不同的线程拥有不同的堆栈。Linux系统为每个线程默认分配了8MB的堆栈空间,如果觉得这个空间不够用,可以通过修改线程的堆栈大小属性进行扩容。

修改线程堆栈大小属性的接口是pthread_attr_setstacksize(),它的完整定义为:

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

它的第二个参数就是堆栈大小了,以字节为单位。需要注意的是,线程堆栈不能小于16KB,而且尽量按4KB(32位系统)或2MB(64位系统)的整数倍分配,也就是内存页面大小的整数倍。此外,修改线程堆栈大小是有风险的,如果你不清楚你在做什么,最好别动它(其实我很后悔把这么危险的东西告诉了你)。

#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <limits.h>

//定义属性变量
pthread_attr_t attr;

void *thread_fun(void *arg)
{
    size_t stacksize;

    printf("I am thread_fun\n");

    pthread_attr_getstacksize(&attr,&stacksize);
    printf("The stacksize is %d\n",(int)stacksize);
    pthread_attr_setstacksize(&attr,16385);

    pthread_attr_getstacksize(&attr,&stacksize);
    printf("The stacksize is %d\n",(int)stacksize);

}

int main(int argc,const char *argv[])
{
    pthread_t tid;
    //线程属性初始化
    pthread_attr_init(&attr);

    //设置为分离状态
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_JOINABLE);

    pthread_attr_setstacksize(&attr,PTHREAD_STACK_MIN);

    if(pthread_create(&tid,&attr,thread_fun,NULL) != 0)
    {
        printf("thread_fun pthread_create eror\n");
    }

    if(pthread_join(tid,NULL) !=0 )
    {
        printf("pthread_join eror\n");
    }

    pthread_attr_destroy(&attr);

    return 0;
}

结果如下所示:

cQZJtU.png

3.2.5满栈警戒区属性

既然线程是有堆栈的,而且还有大小限制,那么就一定会出现将堆栈用满的情况。线程的堆栈用满是非常危险的事情,因为这可能会导致对内核空间的破坏,一旦被有心人士所利用,后果也不堪设想。为了防治这类事情的发生,Linux为线程堆栈设置了一个满栈警戒区。这个区域一般就是一个页面,属于线程堆栈的一个扩展区域。一旦有代码访问了这个区域,就会发出SIGSEGV信号进行通知。

虽然满栈警戒区可以起到安全作用,但是也有弊病,就是会白白浪费掉内存空间,对于内存紧张的系统会使系统变得很慢。所有就有了关闭这个警戒区的需求。同时,如果我们修改了线程堆栈的大小,那么系统会认为我们会自己管理堆栈,也会将警戒区取消掉,如果有需要就要开启它。

修改满栈警戒区属性的接口是pthread_attr_setguardsize(),它的完整定义为:

int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);

它的第二个参数就是警戒区大小了,以字节为单位。与设置线程堆栈大小属性相仿,应该尽量按照4KB或2MB的整数倍来分配。当设置警戒区大小为0时,就关闭了这个警戒区。

虽然栈满警戒区需要浪费掉一点内存,但是能够极大的提高安全性,所以这点损失是值得的。而且一旦修改了线程堆栈的大小,一定要记得同时设置这个警戒区。

3.3线程的同步属性

就像线程有属性一样,线程的同步互斥量也有属性,比较重要的是进程共享属性和类型属性。互斥量的属性用pthread_mutexattr_t类型的数据表示,当然在使用之前必须进行初始化,使用完成之后需要进行销毁:

(1)、pthread_mutexattr_init互斥量的属性初始化函数

int pthread_mutexattr_init(pthread_mutexattr_t*attr)

参数:属性
返回值:成功返回0,失败返回错误码

(2)、pthread_mutexattr_destroy互斥量销毁函数

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr)

参数:属性
返回值:成功返回0,失败返回错误码

进程共享属性有两种值:PTHREAD_PROCESS_PRIVATE,这个是默认值,同一个进程中的多个线程访问同一个同步对象,
PTHREAD_PROCESS_SHARED, 这个属性可以使互斥量在多个进程中进行同步,如果互斥量在多进程的共享内存区域,那么具有这个属性的互斥量可以同步多进程

(3)、pthread_mutexattr_setpshared设置互斥量进程共享属性

int pthread_mutexattr_setpshared(const pthread_mutexattr_t  *attr, int pshared)

第一个参数:共享属性
第二个参数:设置是否共享
返回值:成功返回0,失败返回错误码

(4)、pthread_mutexattr_getpshared获得互斥量进程共享属性

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared)

第一个参数:共享属性
第二个参数:获得属性
返回值:成功返回0,失败返回错误码

进程共享属性需要检测系统是否支持,可以检测宏_POSIX_THREAD_PROCESS_SHARED(#ifdef和#endif检测)。

表1类型属性
互斥量类型 没有解锁时再加锁 不占用时解锁 已解锁时解锁
PTHREAD_MUTEX_NORMAL 死锁 未定义 未定义
PTHREAD_MUTEX_ERRORCHEK 返回错误 返回错误 返回错误
PTHREAD_MUTEX_RECURSIVE 允许 返回错误 返回错误
PTHREAD_MUTEX_DEFAULT 未定义 未定义 未定义

(5)、pthread_mutexattr_settype设置互斥量的类型属性

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type)

第一个参数:类型属性
第二个参数:互斥量类型
返回值:成功返回0,失败返回错误码

(6)、pthread_mutexattr_gettype获取互斥量的类型属性

int pthread_mutexattr_gettype(pthread_mutexattr_t *restrict attr, int *restrict type)

第一个参数:类型属性
第二个参数:互斥量类型
返回值:成功返回0,失败返回错误码

读写锁也有属性,它只有一个进程共享属性:
(7)、pthread_rwlockattr_init读写锁属性初始化函数
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr)
第一个参数:属性
返回值:成功返回0,失败返回错误码
(8)、pthread_rwlockattr_destroy读写锁属性销毁函数

int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr)

第一个参数:属性
返回值:成功返回0,失败返回错误码

(9)、pthread_rwlockattr_setpshared设置读写锁进程共享属性

int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared)

第一个参数:共享属性
第二个参数:设置是否共享
返回值:成功返回0,失败返回错误码

(10)、pthread_rwlockattr_getpshared获取读写锁进程共享属性

int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int *restrict pshared)

第一个参数:共享属性
第二个参数:获取属性
返回值:成功返回0,失败返回错误码

条件变量也有进程共享属性:

(11)、pthread_condattr_init条件变量属性初始化函数

int  int pthread_condattr_init(pthread_condattr_t *attr)

第一个参数:属性
返回值:成功返回0,失败返回错误码

(12)、pthread_condattr_destroy条件变量属性销毁函数

int pthread_condattr_destroy(pthread_condattr_t *attr)

第一个参数:属性
返回值:成功返回0,失败返回错误码

(13)、pthread_condattr_setpshared设置条件变量属性函数

int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared)

第一个参数:共享属性
第二个参数:设置是否共享
返回值:成功返回0,失败返回错误码

(14)、pthread_condattr_getpshared获取条件变量属性函数

int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared)

第一个参数:共享属性
第二个参数:获取的共享
返回值:成功返回0,失败返回错误码

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main()
{
    char *shm = "myshm";
    char *shm1 = "myshm1";
    int shm_id,shm_id1;
    char *buf;
    pid_t pid;

    pthread_mutex_t *mutex;
    pthread_mutexattr_t mutexattr;

    //打开共享内存
    shm_id1=shm_open(shm1, O_RDWR|O_CREAT, 0644);
    //调整共享内存的大小
    ftruncate(shm_id1,100);
    //映射共享内存,MAP_SHARED属性表明,对共享内存的任何修改都会影响其他进程
    mutex = (pthread_mutex_t *)mmap(NULL,100,PROT_READ|PROT_WRITE, MAP_SHARED, shm_id1, 0);
    //属性初始化
    pthread_mutexattr_init(&mutexattr);

//检测系统是否支持
#ifdef _POSIX_THREAD_PROCESS_SHARED
        //设置共享
        pthread_mutexattr_setpshared(&mutexattr,PTHREAD_PROCESS_SHARED);
#endif
    //初始化互斥量
    pthread_mutex_init(mutex,&mutexattr);

    //打开共享内存
    shm_id = shm_open(shm, O_RDWR|O_CREAT, 0644);

    //调整共享内存大小
    ftruncate(shm_id, 100);

    //映射共享内存,MAP_SHARED属性表明,对共享内存的任何修改都会影响其他进程
    buf =(char *)mmap(NULL, 100, PROT_READ|PROT_WRITE, MAP_SHARED, shm_id, 0);

    //创建父子进程
    pid = fork();
    if(pid==0)
    {
        //休眠1s,让父进程先运行
        sleep(1);
        printf("I'm child proccess\n");
        //互斥量加锁
        pthread_mutex_lock(mutex);
        //将共享内存内存修改为hello
        memcpy(buf, "hello", 6);
        printf("child buf is : %s\n", buf);
        //互斥量解锁
        pthread_mutex_unlock(mutex);
    }
    else if(pid>0)
    {
        printf("I'm parent proccess\n");
        //互斥量加锁
        pthread_mutex_lock(mutex);
        //修改共享内存到内容,改为world
        memcpy(buf, "world", 6);
        sleep(3);
        printf("parent buf is : %s\n", buf);
        //互斥量解锁
        pthread_mutex_unlock(mutex);
    }

    //销毁属性
    pthread_mutexattr_destroy(&mutexattr);
    //销毁互斥量
    pthread_mutex_destroy(mutex);

    //解除映射
    munmap(buf, 100);
    //消除共享内存

    shm_unlink(shm);
    //解除映射
    munmap(mutex, 100);

    //消除共享内存
    shm_unlink(shm1);
}

结果如下所示:

cQeGDI.png

3.4线程的私有数据

在单线程程序中,函数经常使用全局变量或静态变量,这是不会影响程序的正确性的,但如果线程调用的函数使用全局变量或静态变量,则很可能引起编程错误,因为这些函数使用的全局变量和静态变量无法为不同的线程保存各自的值,而当同一进程内的不同线程几乎同时调用这样的函数时就可能会有问题发生。而解决这一问题的一种方式就是使用线程私有数据。

线程私有数据采用了一种被称为一键多值的技术,即一个键对应多个数值。访问数据时都是通过键值来访问,好像是对一个变量进行访问,其实是在访问不同的数据。使用线程私有数据时,首先要为每个线程数据创建一个相关联的键。在各个线程内部,都使用这个公用的键来指代线程数据,但是在不同的线程中,这个键代表的数据是不同的。

根据<<unix环境高级编程>>中所涉及的设计线程私有数据的原因:
(1)在维护每个线程的私有数据的时候,我们可能会想到分配一个保存线程数据的数组,用线程的ID 作为数组的索引来实现访问,但是有一个问题是系统生成的线程ID不能保证是一个小而连续的整数, 并且用数组实现的时候由于其他线程也可以访问其数组中的数据,这样会 引起数据混乱。

(2)它提供了让基于进程的接口适应多线程环境的机制.一个很明显的实例就是errno。以前的接口(线程 出现以前)把errno定义为进程上下文中全局可访问的整数。系统调用和库例程在调用或执行失败时设置 errno,把它作为操作失败的附属结果。为了让线程也能够使用那些原本基于进程的系统调用和库例程,errno 被重定义为线程私有数据。这样,一个线程做了重置errno的操作也不会影响进程中其他线程的errno值。

3.5线程与fork

当线程调用fork函数时,就为子进程创建了整个进程地址空间的副本,子进程通过继承这个地址空间的副本,也会将父进程的互斥量、读写锁等状态继承过来。也就是说,如果父进程中互斥量锁着的,那么在子进程中互斥量也是锁着的(尽管子进程还没来得及lock),这是非常不安全的,因为不是子进程自己锁住的,它就无法解锁。

子进程内部只有一个线程,由父进程调用fork函数的线程副本构成。如果调用fork函数的线程将互斥量锁住,那么子进程会拷贝一个pthread_mutex_lock副本,这样子进程就有机会去解锁了。

或者互斥量根本就没有被加锁,这样也是可以的,但是你不能确定永远是这样的情况。
pthread_atfork()在fork()之前调用,当调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child。

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void prepare(void)
{
    pthread_mutex_lock(&mutex);
    printf("I am prepare\n");
}

void parent(void)
{
    pthread_mutex_unlock(&mutex);
    printf("I am parent\n");
}

void child(void)
{
    pthread_mutex_unlock(&mutex);
    printf("I am child\n");
}

void *pthread_fun(void *arg)
{
    sleep(1);
    pid_t pid;
    pthread_atfork(prepare,parent,child);
    pid = fork();

    if(pid == 0)
    {
        pthread_mutex_lock(&mutex);
        printf("child process\n");
        pthread_mutex_unlock(&mutex);
    }
    if(pid > 0)
    {
        pthread_mutex_lock(&mutex);
        printf("parent process\n");
        pthread_mutex_unlock(&mutex);
    }
}

int main(void)
{
    pthread_t tid;

    if(pthread_create(&tid, NULL, pthread_fun, NULL) != 0)
    {
        printf("pthread_create error\n");
    }

    pthread_join(tid, NULL);

    return 0;
}

结果如下所示:

cQeBvj.png

Related posts

Leave a Comment